
function expovariate(taxa) {
	// Utilizamos a técnica da inversa da função cumulativa.
	// Obs: Math.log já é na base neperiana.
	var u;
	do {
		u = Math.random();
	} while(u == 0);
	var x = -1.0/taxa * Math.log(u);
	if (x <= 0) {
		throw "assert failed (x > 0)";
	}
	return x;
}

function Profile(simulador) {
	this.nr_seeds = [];
	this.nr_peers = [];
	this.tempo = [];
	this.nr_peers_medio = [ 0 ];
	this.nr_peers_qtd = 0;
	this.tempo_download_medio = [ 0 ];
	this.tempo_download_qtd = 0;
	this.vazao_por_rodada = []; 

	this.simulador = simulador;
}
Profile.prototype = {
	add_sample: function() {
		var qtd = 0;
		var tempo_download = 0;
		var dt0 = this.simulador.tempo_atual;
		var dt_1 = this.simulador.tempo_atual - this.simulador.settings.dt;

		seeds = this.simulador.seeds_download_agora;
		for(var j=0; j<seeds.length; j++) {
			if(seeds[j].virou_seed.tempo < dt0 && seeds[j].virou_seed.tempo > dt_1) {
				tempo_download += seeds[j].virou_seed.tempo - seeds[j].chegada.tempo;
				qtd++;
			}
		}

		if (this.simulador.tempo_atual > 0) {
			this.nr_seeds.push(this.simulador.seeds_ativos.length);
			this.nr_peers.push(this.simulador.peers_ativos.length);
			this.tempo.push(this.simulador.tempo_atual);

			// Nr médio de peers.
			var nr_peers = this.simulador.peers_ativos.length;
			nr_peers += this.nr_peers_qtd * this.nr_peers_medio[this.nr_peers_medio.length - 1];
			this.nr_peers_qtd++;
			nr_peers /= 1.0 * this.nr_peers_qtd;
			this.nr_peers_medio.push(nr_peers);

			// Tempo de download médio.
			tempo_download += this.tempo_download_qtd * this.tempo_download_medio[this.tempo_download_medio.length-1];
			this.tempo_download_qtd += qtd;
			tempo_download /= 1.0 * this.tempo_download_qtd;
			this.tempo_download_medio.push(tempo_download);
		}
		this.simulador.seeds_download_agora = [];
	}
}

function User(simulador, nome, classe) {
	this.ativo = true;
	this.nome = nome;
	this.classe = classe;
	this.blocos = [];
	this.simulador = simulador;

	// Profile
	this.chegada = {};
	this.virou_seed = {};
	this.saida = {};
	this.blocos_enviados = 0;
	this.blocos_nao_enviados = 0;
	this.nr_agenda_upload = 0;

	// Seta taxa de upload.
	if (classe == User.CLASSES.PEER) {
		this.taxa_upload = simulador.settings.mi;
		// O peer começa 0 blocos.
		for(var i=0; i<simulador.settings.b; i++) {
			this.blocos.push(false);
		}
	} else if (classe == User.CLASSES.PUBLISHER) {
		this.taxa_upload = simulador.settings.u;
		// O publisher tem todos os blocos.
		for(var i=0; i<simulador.settings.b; i++) {
			this.blocos.push(true);
		}
	}

	return this;
}
User.CLASSES = {
	PEER: 'Peer',
	SEED: 'Seed',
	PUBLISHER: 'Publisher'
}
User.prototype = {
	agenda_saida: function() {
		var dt;
		if (this.simulador.settings.gamma == Simulador.GAMMA_INF) {
			dt = 0;
		} else {
			dt = expovariate(this.simulador.settings.gamma);
		}
		this.simulador.add_evento(dt, new EventoSeedSaida(this.simulador, this));
	},
	agenda_upload: function() {
		var dt = expovariate(this.taxa_upload);
		this.nr_agenda_upload++;
		this.simulador.add_evento(dt, new EventoUserUpload(this.simulador, this));
	}
}

function Simulador(settings) {
	// Parâmetros: lambda, b, mi, u, gamma, p, n0, blocos_iniciais, nr_rodadas, qtde_eventos, tempo_fase_transiente, dt.
	this.settings = settings;

	// Usuários que estão ativos no sistema.
	// Deixamos separados por tipo para facilitar os acessos no código.
	this.publishers_ativos = [];
	this.peers_ativos = [];
	this.seeds_ativos = [];

	// Seeds que sairam do sistema ao longo da simulação.
	this.seeds_inativos = [];

	// Tempo da simulação.
	this.tempo_atual = 0;

	// Rodada da simulação.
	// A rodada 0 sempre é a fase transiente.
	this.rodada_atual = 0;

	// Informações sobre as rodadas.
	this.rodadas = []

	// Fila de eventos.
	this.fila_eventos = [];

	// Qtde de peers que contém.
	this.qtd_peers_blocos = [];

	// Utilizado para cancelar a execução de uma simulação.
	// O estado do sistema se mantém consistente e sua execução
	// pode ser continuada apenas chamando run() novamente.
	this.cancelar = false;

	// Indica que a simulação foi concluída.
	this.concluido = false;

	this.blocos_enviados = 0;
	this.blocos_nao_enviados_peer = 0;
	this.blocos_nao_enviados_bloco = 0;

	this.seeds_download_agora = [];

	return this;
}
Simulador.GAMMA_INF = -1;
Simulador.ALEATORIO = 1;
Simulador.RARO = 2;
Simulador.prototype = {
	log: function(mensagem) {
		//console.log('['+this.tempo_atual+'] '+mensagem);
	},
	bloco_mais_raro: function(candidatos) {
		var qtd = this.qtd_peers_blocos[candidatos[0]];
		var bloco = candidatos[0];

		for(var i=0; i<candidatos.length; i++) {
			if (this.qtd_peers_blocos[candidatos[i]] <= qtd) {
				bloco = candidatos[i];
				qtd = this.qtd_peers_blocos[candidatos[i]];
			}
		}
		return bloco;
	},
	add_peer_ativo: function(nome) {
		this.log('Adicionado peer '+nome);

		// Instancia o peer.
		var peer = new User(this, nome, User.CLASSES.PEER);

		// Profile.
		peer.chegada.tempo = this.tempo_atual;
		peer.chegada.rodada = this.rodada_atual;

		// Adiciona o User aos peers ativos do simulador.
		this.peers_ativos.push(peer);
		return peer
	},
	add_evento: function(dt, evento) {
		// Adiciona ao vetor e ordena invertido pelo tempo.
		// Desta maneira, o próximo evento é o último elemento do vetor.
		// FIXME Utilizar priority queue por motivos de performance.
		this.fila_eventos.push({ tempo: this.tempo_atual + dt, evento: evento });
		this.fila_eventos.sort(function(a, b) {
			return b.tempo - a.tempo;
		});
	},
	next_evento: function() {
		return this.fila_eventos.pop();
	},
	prepare: function() {
		if (this.settings.lambda > 0) {
			this.add_evento(0, new EventoPeerChegadaExterna(this));
		}

		// inicializa publisher.
		var publisher = new User(this, 'P1', User.CLASSES.PUBLISHER);
		this.publishers_ativos.push(publisher);
		publisher.agenda_upload();

		// inicializa qtd de peers por blocos
		for(var i=0; i<this.settings.b; i++) {
			this.qtd_peers_blocos[i] = 0;
		}

		// inicializa população inicial
		for(var i=0; i < this.settings.n0; i++) {
			var nome = i;
			var peer = this.add_peer_ativo(nome);

			//blocos iniciais
			if (this.settings.blocos_iniciais > 0) {
				for(var j=0; j < this.settings.blocos_iniciais; j++) {
					peer.blocos[j] = true;
					this.qtd_peers_blocos[j]++;
				}
				// tendo bloco inicial, o peer já pode fazer upload
				peer.agenda_upload();
			}
		}

		// inicializa profile.
		this.profile = new Profile(this);
		var dt = this.settings.dt;
		this.add_evento(dt, new EventoUserProfile(this, this.profile, dt));

		// inicializa variáveis internas.
		this._counter = 0;
		this._acabou_rodada = false;

		// cria rodada 0
		this.rodadas.push({});
		this.rodadas[this.rodada_atual].tempo_inicio = this.tempo_atual;
	},
	run: function(callback) {
		var item;

		var start = new Date();
		while ((new Date() - start) < 1000 && this.rodada_atual <= this.settings.nr_rodadas && !this.cancelada) {
			item = this.next_evento();

			// Atualiza o tempo atual.
			this.tempo_atual = item.tempo;

			if (this.rodada_atual == 0) {
				//critério de parada da fase transiente é por tempo
				acabou_rodada = (this.tempo_atual > this.settings.tempo_fase_transiente);
			} else {
				//critério de parada das rodadas  é por eventos processados
				acabou_rodada = (this._counter > this.settings.qtde_eventos);
			}
				
			// Executa o evento.
			if (item.evento.run()) {
				this._counter++;
			}

			if (acabou_rodada) {
				this._counter = 0;
				this._acabou_rodada = false;
				this.rodadas[this.rodada_atual].tempo_fim = this.tempo_atual;

				if (this.rodada_atual == this.settings.nr_rodadas) {
					// Concluí simulação.
					this.concluido = true;
					if (callback) {
						callback(this);
					}
					break;
				}

				this.rodada_atual++;
				this.rodadas.push({});
				this.rodadas[this.rodada_atual].tempo_inicio = this.tempo_atual;
			}
		}
				
		var self = this;
		if(!this.concluido && !this.cancelada) {
			setTimeout(function() {
				self.run(callback);
			}, 1);
		}

		return this.profile;
	}
}

